WPF笔记(X1) Threading Model (四)

这一篇整理一下DispatcherTimer. 看DispatcherTimer之前, 先看一下.Net中的其他Timer

Timer(C# in a nutshell 6th)
The .NET Framework provides four timers. Two of these are general-purpose multithreaded timers:

  • System.Threading.Timer
  • System.Timers.Timer

The other two are special-purpose single-threaded timers:

  • System.Windows.Forms.Timer (Windows Forms timer)
  • System.Windows.Threading.DispatcherTimer (WPF timer)

The multithreaded timers are more powerful, accurate, and flexible; the singlethreaded timers are safer and more convenient for running simple tasks that update Windows Forms controls or WPF elements.
System.Threading.Timer is the simplest multithreaded timer: it has just a constructor and two methods

The .NET Framework provides another timer class of the same name in the System.Timers namespace. This simply wraps the System.Threading.Timer (参考:https://www.gnu.org/software/dotgnu/pnetlib-doc/System/Threading/Timer.html)

Multithreaded timers use the thread pool to allow a few threads to serve many timers. This means that the callback method or Elapsed event may fire on a different thread each time it is called. Furthermore, the Elapsed event always fires(approximately) on time—regardless of whether the previous Elapsed event finished executing. Hence, callbacks or event handlers must be thread-safe.

使用的是线程池, 回调函数有可能是在不同的线程中被调用. 不管前一个调用是否完成, 后一个调用会如期而至, 因此回调函数必须是线程安全的(可重入的)

定时器精度在10-20ms之间

而DispatcherTimer的时间精度就没那么准(参考:https://msdn.microsoft.com/en-us/library/system.windows.threading.dispatchertimer.interval(v=vs.110).aspx)

Timers are not guaranteed to execute exactly when the time interval occurs, but they are guaranteed to not execute before the time interval occurs. This is because DispatcherTimer operations are placed on the Dispatcher queue like other operations. When the DispatcherTimer operation executes is dependent on the other jobs in the queue and their priorities.

如果要计时的话, 用stopwatch, 参考https://stackoverflow.com/questions/4251644/getting-elapsed-time-with-dispatchtimer-to-1-millisecond-accuracy
https://social.msdn.microsoft.com/forums/windowsapps/en-us/058a755e-059b-45ee-b5ec-3d35f3b53515/dispatchertimer-accuracy

DispatcherTimer这个类不复杂, 阅读它的代码能学到不少东西, 可以进一步了解Dispatcher的运行方式.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
using System;
using System.Threading;
using System.Windows;
using System.Diagnostics;
using System.Collections.Generic;
using MS.Internal.WindowsBase;

namespace System.Windows.Threading
{
/// <summary>
/// A timer that is integrated into the Dispatcher queues, and will
/// be processed after a given amount of time at a specified priority.
/// </summary>
public class DispatcherTimer
{
/// <summary>
/// Creates a timer that uses the current thread's Dispatcher to
/// process the timer event at background priority.
/// </summary>
public DispatcherTimer() : this(DispatcherPriority.Background) // NOTE: should be Priority Dispatcher.BackgroundPriority
{
}

/// <summary>
/// Creates a timer that uses the current thread's Dispatcher to
/// process the timer event at the specified priority.
/// </summary>
/// <param name="priority">
/// The priority to process the timer at.
/// </param>
public DispatcherTimer(DispatcherPriority priority) // NOTE: should be Priority
{
Initialize(Dispatcher.CurrentDispatcher, priority, TimeSpan.FromMilliseconds(0));
}

/// <summary>
/// Creates a timer that uses the specified Dispatcher to
/// process the timer event at the specified priority.
/// </summary>
/// <param name="priority">
/// The priority to process the timer at.
/// </param>
/// <param name="dispatcher">
/// The dispatcher to use to process the timer.
/// </param>
public DispatcherTimer(DispatcherPriority priority, Dispatcher dispatcher) // NOTE: should be Priority
{
if(dispatcher == null)
{
throw new ArgumentNullException("dispatcher");
}

Initialize(dispatcher, priority, TimeSpan.FromMilliseconds(0));
}

/// <summary>
/// Creates a timer that is bound to the specified dispatcher and
/// will be processed at the specified priority, after the
/// specified timeout.
/// </summary>
/// <param name="interval">
/// The interval to tick the timer after.
/// </param>
/// <param name="priority">
/// The priority to process the timer at.
/// </param>
/// <param name="callback">
/// The callback to call when the timer ticks.
/// </param>
/// <param name="dispatcher">
/// The dispatcher to use to process the timer.
/// </param>
public DispatcherTimer(TimeSpan interval, DispatcherPriority priority, EventHandler callback, Dispatcher dispatcher) // NOTE: should be Priority
{
//
if(callback == null)
{
throw new ArgumentNullException("callback");
}
if(dispatcher == null)
{
throw new ArgumentNullException("dispatcher");
}

if (interval.TotalMilliseconds < 0)
throw new ArgumentOutOfRangeException("interval", SR.Get(SRID.TimeSpanPeriodOutOfRange_TooSmall));

if (interval.TotalMilliseconds > Int32.MaxValue)
throw new ArgumentOutOfRangeException("interval", SR.Get(SRID.TimeSpanPeriodOutOfRange_TooLarge));

Initialize(dispatcher, priority, interval);

Tick += callback;
Start();
}

/// <summary>
/// Gets the dispatcher this timer is associated with.
/// </summary>
public Dispatcher Dispatcher
{
get
{
return _dispatcher;
}
}

/// <summary>
/// Gets or sets whether the timer is running.
/// </summary>
public bool IsEnabled
{
get
{
return _isEnabled;
}

set
{
lock(_instanceLock)
{
if(!value && _isEnabled)
{
Stop();
}
else if(value && !_isEnabled)
{
Start();
}
}
}
}

/// <summary>
/// Gets or sets the time between timer ticks.
/// </summary>
public TimeSpan Interval
{
get
{
return _interval;
}

set
{
bool updateWin32Timer = false;

if (value.TotalMilliseconds < 0)
throw new ArgumentOutOfRangeException("value", SR.Get(SRID.TimeSpanPeriodOutOfRange_TooSmall));

if (value.TotalMilliseconds > Int32.MaxValue)
throw new ArgumentOutOfRangeException("value", SR.Get(SRID.TimeSpanPeriodOutOfRange_TooLarge));

lock(_instanceLock)
{
_interval = value;

if(_isEnabled)
{
_dueTimeInTicks = Environment.TickCount + (int)_interval.TotalMilliseconds;
updateWin32Timer = true;
}
}

if(updateWin32Timer)
{
_dispatcher.UpdateWin32Timer();
}
}
}

/// <summary>
/// Starts the timer.
/// </summary>
public void Start()
{
lock(_instanceLock)
{
if(!_isEnabled)
{
_isEnabled = true;

Restart();
}
}
}

/// <summary>
/// Stops the timer.
/// </summary>
public void Stop()
{
bool updateWin32Timer = false;

lock(_instanceLock)
{
if(_isEnabled)
{
_isEnabled = false;
updateWin32Timer = true;

// If the operation is in the queue, abort it.
if(_operation != null)
{
_operation.Abort();
_operation = null;
}

}
}

if(updateWin32Timer)
{
_dispatcher.RemoveTimer(this);
}
}

/// <summary>
/// Occurs when the specified timer interval has elapsed and the
/// timer is enabled.
/// </summary>
public event EventHandler Tick;

/// <summary>
/// Any data that the caller wants to pass along with the timer.
/// </summary>
public object Tag
{
get
{
return _tag;
}

set
{
_tag = value;
}
}


private void Initialize(Dispatcher dispatcher, DispatcherPriority priority, TimeSpan interval)
{
// Note: all callers of this have a "priority" parameter.
Dispatcher.ValidatePriority(priority, "priority");
if(priority == DispatcherPriority.Inactive)
{
throw new ArgumentException(SR.Get(SRID.InvalidPriority), "priority");
}

_dispatcher = dispatcher;
_priority = priority;
_interval = interval;
}

private void Restart()
{
lock(_instanceLock)
{
if (_operation != null)
{
// Timer has already been restarted, e.g. Start was called form the Tick handler.
return;
}

// BeginInvoke a new operation.
_operation = _dispatcher.BeginInvoke(
DispatcherPriority.Inactive,
new DispatcherOperationCallback(FireTick),
null);


_dueTimeInTicks = Environment.TickCount + (int) _interval.TotalMilliseconds;

if (_interval.TotalMilliseconds == 0 && _dispatcher.CheckAccess())
{
// shortcut - just promote the item now
Promote();
}
else
{
_dispatcher.AddTimer(this);
}
}

}

internal void Promote() // called from Dispatcher
{
lock(_instanceLock)
{
// Simply promote the operation to it's desired priority.
if(_operation != null)
{
_operation.Priority = _priority;
}
}
}

private object FireTick(object unused)
{
// The operation has been invoked, so forget about it.
_operation = null;

// The dispatcher thread is calling us because item's priority
// was changed from inactive to something else.
if(Tick != null)
{
Tick(this, EventArgs.Empty);
}

// If we are still enabled, start the timer again.
if(_isEnabled)
{
Restart();
}

return null;
}

// This is the object we use to synchronize access.
private object _instanceLock = new object();

// Note: We cannot BE a dispatcher-affinity object because we can be
// created by a worker thread. We are still associated with a
// dispatcher (where we post the item) but we can be accessed
// by any thread.
private Dispatcher _dispatcher;

private DispatcherPriority _priority; // NOTE: should be Priority
private TimeSpan _interval;
private object _tag;
private DispatcherOperation _operation;
private bool _isEnabled;

internal int _dueTimeInTicks; // used by Dispatcher
}
}

从DispatcherTimer的使用方式入手

1
2
3
4
5
//  DispatcherTimer setup
dispatcherTimer = new System.Windows.Threading.DispatcherTimer();
dispatcherTimer.Tick += new EventHandler(dispatcherTimer_Tick);
dispatcherTimer.Interval = new TimeSpan(0,0,1);
dispatcherTimer.Start();

无参构造方法DispatcherTimer()将DispatcherPriority设为Background

1
2
3
4
5
6
7
/// <summary>
/// Creates a timer that uses the current thread's Dispatcher to
/// process the timer event at background priority.
/// </summary>
public DispatcherTimer() : this(DispatcherPriority.Background) // NOTE: should be Priority Dispatcher.BackgroundPriority
{
}

Background优先级很低, 所以不一定能够按时执行.

再看Start方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
/// <summary>
/// Starts the timer.
/// </summary>
public void Start()
{
lock(_instanceLock)
{
if(!_isEnabled)
{
_isEnabled = true;

Restart();
}
}
}

调用的是Restart()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
private void Restart()
{
lock(_instanceLock)
{
if (_operation != null)
{
// Timer has already been restarted, e.g. Start was called form the Tick handler.
return;
}

// BeginInvoke a new operation.
_operation = _dispatcher.BeginInvoke(
DispatcherPriority.Inactive,
new DispatcherOperationCallback(FireTick),
null);


_dueTimeInTicks = Environment.TickCount + (int) _interval.TotalMilliseconds;

if (_interval.TotalMilliseconds == 0 && _dispatcher.CheckAccess())
{
// shortcut - just promote the item now
Promote();
}
else
{
_dispatcher.AddTimer(this);
}
}

}

这个方法比较关键

  1. 它往Dispatcher Queue里面放了一个Operation, 但它的优先级是DispatcherPriority.Inactive. 顾名思义, 这个优先级表示该Operation是不激活状态. 它不会被执行
    1
    2
    3
    4
    /// <summary>
    /// Operations at this priority are not processed.
    /// </summary>
    Inactive = 0,

因为时间间隔不到, Operation不能被执行. 只有时间间隔到了, Operation才会被激活. 如何激活? 且看下面

  1. Promote()方法, 就是激活该Operation. Promote是提升的意思, 也就是提升Operation的优先级.
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    internal void Promote() // called from Dispatcher
    {
    lock(_instanceLock)
    {
    // Simply promote the operation to it's desired priority.
    if(_operation != null)
    {
    _operation.Priority = _priority;
    }
    }
    }

_priority构造的时候为DispatcherPriority.Background. 即从DispatcherPriority.Inactive提升(Promote)到
DispatcherPriority.Background. 如此, Operation就可以被执行了.

注意called from Dispatcher, 表示, 时间间隔到了, 提升Operation的优先级是由Dispatcher来完成的.
Dispatcher和DispatcherTimer有什么关系呢? 且看下面

  1. _dispatcher.AddTimer(this);
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    internal void AddTimer(DispatcherTimer timer)
    {
    lock(_instanceLock)
    {
    if(!_hasShutdownFinished) // Could be a non-dispatcher thread, lock to read
    {
    _timers.Add(timer);
    _timersVersion++;
    }
    }
    UpdateWin32Timer();
    }

添加了一个DispatcherTimer, 需要UpdateWin32Timer, 去操作系统设置定时器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
internal void UpdateWin32Timer() // Called from DispatcherTimer
{
if(CheckAccess())
{
UpdateWin32TimerFromDispatcherThread(null);
}
else
{
BeginInvoke(DispatcherPriority.Send,
new DispatcherOperationCallback(UpdateWin32TimerFromDispatcherThread),
null);
}
}

private object UpdateWin32TimerFromDispatcherThread(object unused)
{

lock(_instanceLock)
{
if(!_hasShutdownFinished) // Dispatcher thread, does not technically need the lock to read
{
bool oldDueTimeFound = _dueTimeFound;
int oldDueTimeInTicks = _dueTimeInTicks;
_dueTimeFound = false;
_dueTimeInTicks = 0;

if(_timers.Count > 0)
{
// We could do better if we sorted the list of timers.
for(int i = 0; i < _timers.Count; i++)
{
DispatcherTimer timer = _timers[i];

if(!_dueTimeFound || timer._dueTimeInTicks - _dueTimeInTicks < 0)
{
_dueTimeFound = true;
_dueTimeInTicks = timer._dueTimeInTicks;
}
}
}

if(_dueTimeFound)
{
if(!_isWin32TimerSet || !oldDueTimeFound || (oldDueTimeInTicks != _dueTimeInTicks))
{
SetWin32Timer(_dueTimeInTicks);
}
}
else if(oldDueTimeFound)
{
KillWin32Timer();
}
}
}

return null;
}

///<SecurityNote>
/// Critical - accesses critical data
/// TreatAsSafe - we think it's ok to expose timers in the SEE.
/// a denial-of-service attack may be possible - but these are low-pri and possible in many other ways.
/// we can never bring down the iexplore process.
///</SecurityNote>
[SecurityCritical, SecurityTreatAsSafe]
private void SetWin32Timer(int dueTimeInTicks)
{
if(!IsWindowNull())
{
int delta = dueTimeInTicks - Environment.TickCount;
if(delta < 1)
{
delta = 1;
}

// We are being called on the dispatcher thread so we can rely on
// _window.Value being non-null without taking the instance lock.

SafeNativeMethods.SetTimer(
new HandleRef(this, _window.Value.Handle),
TIMERID_TIMERS,
delta);

_isWin32TimerSet = true;
}
}

这样, 系统才会发WM_TIMER给窗口处理函数, Dispatcher才会从DispatcherQueue里面取出Operation, 看下面的代码

1
2
3
4
5
6
7
8
9
10
private IntPtr WndProcHook(IntPtr hwnd, int msg, IntPtr wParam, IntPtr lParam, ref bool handled)

else if(message == WindowMessage.WM_TIMER && (int) wParam == TIMERID_TIMERS)
{
// We want 1-shot only timers. So stop the timer
// that just fired.
KillWin32Timer();

PromoteTimers(Environment.TickCount);
}

PromoteTimers()判断时间间隔有没有到; 如果到了,调用的是timer.Promote()方法, 提升Operation的优先级

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
internal void PromoteTimers(int currentTimeInTicks)

while(iTimer < _timers.Count)
{
// WARNING: this is vulnerable to wrapping
if(timers[iTimer]._dueTimeInTicks - currentTimeInTicks <= 0) //判断时间有没有到
{
// Remove this timer from our list.
// Do not increment the index.
timer = timers[iTimer];
timers.RemoveAt(iTimer);
break;
}
else
{
iTimer++;
}
}

// Now that we are outside of the lock, promote the timer.
if(timer != null)
{
timer.Promote();
}

  1. 时间间隔到了, 执行FireTick方法
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    private object FireTick(object unused)
    {
    // The operation has been invoked, so forget about it.
    _operation = null;

    // The dispatcher thread is calling us because item's priority
    // was changed from inactive to something else.
    if(Tick != null)
    {
    Tick(this, EventArgs.Empty);
    }

    // If we are still enabled, start the timer again.
    if(_isEnabled)
    {
    Restart();//再把op放到queue中
    }

    return null;
    }

前面由于DispatcherOperation已经从DispatcherQueue里面取出, 所以这里需要重新再向DispatcherQueue放入DispatcherOperation, 等下一个时间间隔来临时执行. 做法是, 再次调用Restart方法.